Smart contract functionality via revive pallet#95
Conversation
Adds pallet_revive 0.13.0 to the storage-parachain-runtime and
storage-paseo-runtime at index 60, mirroring the asset-hub-westend
integration pattern from polkadot-sdk@stable2603:
- TxExtension extended with pallet_revive::evm::tx_extension::SetOrigin
so Ethereum-flavored transactions reach the substrate runtime.
- UncheckedExtrinsic swapped to pallet_revive's wrapper.
- WeightToFee replaced with BlockRatioFee (revive's gas/weight contract
is enforced at compile time).
- impl_runtime_apis! → impl_runtime_apis_plus_revive_traits!.
- FeeMultiplierUpdate switched from () to SlowAdjustingFeeUpdate;
revive's gas-price integrity check rejects a zero min multiplier.
- Distinct ChainIDs (420_420_500 / 420_420_501).
- AccountId32Mapper as the H160 ↔ AccountId32 bridge.
- Precompiles = () placeholder; the storage-provider and drive-registry
precompile crates land in the next commit.
paseo's runtimes/web3-storage-paseo/tests/tests.rs is updated for the
new 9-element TxExtension and revive's wrapper constructors.
Issue #83.
…ecompiles (issue #83 step 2) Adds two custom precompile crates wrapping the client-side surface of pallet_storage_provider and pallet_drive_registry, both implementing the `pallet_revive::precompiles::Precompile` trait per the canonical XCM precompile pattern from polkadot-sdk@stable2603. - precompiles/storage-provider-precompile (matcher Fixed(0x0901), address 0x…09010000): 11 selectors — createBucket, createBucketWithStorage, freezeBucket, setMember, removeMember, requestPrimaryAgreement, topUpAgreement, extendAgreement, endAgreementPay, endAgreementBurn, challengeCheckpoint. - precompiles/drive-registry-precompile (matcher Fixed(0x0902), address 0x…09020000): createDrive, deleteDrive, shareDrive, unshareDrive. Both wire into the Precompiles tuple of pallet_revive::Config in both runtime/src/revive.rs and runtimes/web3-storage-paseo/src/revive.rs. The `Fixed(p)` matcher places the u16 at bytes 16-17 of the H160 with a 0x0000 suffix at bytes 18-19 (bytes 18-19 are reserved for built-in precompiles). Each branch: read-only guard → derive `RawOrigin::Signed(env.caller())` → env.charge(WeightInfo) → SCALE-decode bytes32 provider/member args into AccountId → dispatch into the pallet → ABI-encode any return. Issue #83.
…ep 3)
Example dApp showing how a Solidity contract uses the storage-provider
precompile to buy storage on behalf of its users:
- examples/contracts/IWeb3Storage.sol — vendored copy of the precompile
ABI (kept in sync manually).
- examples/contracts/IDriveRegistry.sol — vendored copy of the
drive-registry precompile ABI (used by sc-coverage.js).
- examples/contracts/StorageMarketplace.sol — `buyStorage{value}`
forwards msg.value to createBucketWithStorage and records the
msg.sender → bucketId mapping; `endMyAgreement(bucketId, bytes32
provider)` checks ownership and pays the provider.
- examples/contracts/build.sh — invokes `resolc --combined-json abi,bin`
against all three .sol files, emitting `build/combined.json` keyed
by `<file>:<contract>`. Single artifact is easier for the e2e
driver to load than per-contract .bin / .abi pairs.
- examples/contracts/README.md — install hints + version pins
(matched against .github/env's SOLC_VERSION / RESOLC_VERSION).
- examples/contracts/.gitignore — excludes build/.
The precompile address constant in StorageMarketplace.sol is
`0x0000000000000000000000000000000009010000`, matching the matcher
layout: `Fixed(0x0901)` places `0x0901` at bytes 16-17 with `0x0000`
suffix at bytes 18-19.
Issue #83.
…tep 4)
Two test drivers and a small helper module — all PAPI-only on the
chain side, viem only for ABI encoding/decoding (no EVM JSON-RPC).
- examples/papi/sc-api.js — helpers for driving pallet_revive from
PAPI. `deployContract` wraps Revive.instantiate_with_code,
`callContract` wraps Revive.call (v2 dispatchables only, per the
runtime metadata's snake_case fields: weight_limit, dest, etc.),
`ensureAccountMapped` idempotent-calls Revive.map_account (required
before any substrate account can be a contract caller or value-
transfer target), `decodeContractEmitted` finds ContractEmitted
events matching a given H160 and decodes against an ABI.
- examples/papi/sc-flow.js — `just sc-demo`: deploy
StorageMarketplace.sol, call buyStorage with msg.value, off-chain
upload + challenge round-trip, end via contract; asserts the
provider earned tokens and the contract event fired.
- examples/papi/sc-coverage.js — `just sc-coverage`: direct
Revive.call(precompileAddr, calldata) for every selector on both
precompiles. Chains preconditions where needed (creates a fresh
bucket per agreement-lifecycle step; uploads + checkpoints before
challengeCheckpoint/freezeBucket; uses a large agreement for
endAgreementBurn so 10% of the payment exceeds the existential
deposit).
- examples/papi/package.json — adds viem dep and the demo:sc-flow
npm script.
`weight_limit.ref_time` defaults to 1s — block max with
MaxEthExtrinsicWeight = 9/10 is 1.8s, so anything ≥ 2s gets rejected
with `Invalid::ExhaustsResources`.
Issue #83.
…step 5)
Adds three justfile recipes for the smart-contract workflow:
- build-contracts: shells into examples/contracts/build.sh, which
requires solc + resolc on PATH.
- sc-demo: full marketplace e2e via examples/papi/sc-flow.js.
Depends on papi-setup (descriptor regen + npm install).
- sc-coverage: per-selector coverage via examples/papi/sc-coverage.js.
Same papi-setup dependency.
Both demos accept the standard `PROVIDER_URL PROVIDER_SEED CLIENT_SEED`
positional args used by the rest of the demo recipes.
Issue #83.
Pins SOLC_VERSION=0.8.35 and RESOLC_VERSION=1.1.0 in .github/env (per
the source-of-truth-for-CI-versions convention) and adds three steps
to integration-tests.yml after the existing PAPI demos:
- Install solc + resolc from GitHub releases into /usr/local/bin.
- just build-contracts — compile StorageMarketplace.sol (+ the
vendored interfaces) into examples/contracts/build/combined.json.
- just drain-tx-pool-then sc-demo — marketplace e2e.
- just drain-tx-pool-then sc-coverage — per-selector coverage.
Both demos run across every matrix runtime entry automatically because
the matrix iterates the same job body.
Issue #83.
…tep 7)
- docs/design/smart-contracts.md — new design doc: precompile address
layout (Fixed(p) places p at bytes 16-17 with 0x0000 suffix at
bytes 18-19), per-selector ABI tables for both precompiles, the
substrate↔EVM type encoding rules (bytes32 for AccountId32, uint8
for Role/burnPercent enum tags, etc.), origin model via
AccountId32Mapper, payment flow through msg.value, weight metering,
"adding a new selector" walkthrough, "testing" section pointing at
sc-flow.js / sc-coverage.js, and v1 limits / follow-ups.
- docs/README.md — link the new design doc under Design.
- CLAUDE.md — directory map entries for precompiles/ and
examples/contracts/, plus a short Architecture subsection pointing
at the new doc.
Issue #83.
The previous 1 MiB default was just under what `deleteDrive` actually needs — a dry-run via `ReviveApi.call` reported `proof_size: 1053490`, so the second-to-last byte tipped the call into OOG. Bumping to 4 MiB (comfortably under the ~5 MiB block POV cap) gives every selector exercised by sc-coverage.js plenty of headroom. With this, sc-coverage.js runs all 15 selectors green end-to-end. Issue #83.
Adds a third custom precompile wrapping pallet_s3_registry's full client-side surface (6 selectors: createS3Bucket, createS3BucketWithStorage, deleteS3Bucket, putObjectMetadata, deleteObjectMetadata, copyObjectMetadata). Address: 0x0000000000000000000000000000000009030000, matcher Fixed(0x0903) — same layout convention as the storage-provider and drive-registry precompiles. `put_object_metadata`'s `user_metadata: Vec<(Vec<u8>, Vec<u8>)>` is dropped from the Solidity surface in v1 (always passes an empty vec); the ABI for nested dynamic-bytes tuples is awkward and no current dApp needs it. Documented in docs/design/smart-contracts.md. Wires into the Precompiles tuple of both runtimes alongside the existing two. Issue #83.
Drive-registry dApp showing how a contract owns a drive and manages
membership through the precompile. Surface:
- createTeam(name, maxCapacity, storagePeriod, payment, minProviders)
payable → registers caller as admin and creates the drive.
- invite(bytes32 member, uint8 role) — admin only → shareDrive.
- kick(bytes32 member) — admin only → unshareDrive.
- disband() — admin only → deleteDrive.
`admin != address(0)` is the "team exists" sentinel; we can't use
`driveId != 0` because the chain assigns `drive_id` starting at 0.
Includes `examples/papi/sc-team-drive.js` (PAPI e2e: deploy → createTeam
→ invite → kick → disband, asserts DriveRegistry pallet events + the
contract's TeamCreated/Invited/Kicked/Disbanded events) and CI step.
Issue #83.
S3-registry dApp showing the access-gating pattern: the contract owns
an S3 bucket and mints a transferable NFT-shaped token per object
stored in it. Surface:
- initialize(name, maxCapacity, duration, maxPayment) payable
→ createS3BucketWithStorage; caller becomes publisher.
- mint(address to, key, cid, size, contentType) — publisher only
→ putObjectMetadata; assigns tokenId; emits ERC721-shaped Transfer
from address(0).
- transfer(address to, uint256 tokenId) — current owner only.
- burn(uint256 tokenId) — owner or publisher; deletes the underlying
S3 object metadata too.
- shutdown() — publisher only; bucket must be empty.
Minimal ERC721-shaped surface (balanceOf, ownerOf, transfer) — not a
full ERC721 (no approvals, no safeTransfer hooks). Demo, not a
marketplace primitive.
`publisher != address(0)` is the "initialized" sentinel — the chain
assigns `s3_bucket_id` starting at 0, same gotcha as drive_id in
SharedTeamDrive.
Includes:
- examples/papi/sc-token-gated.js — PAPI e2e: publisher (//Bob)
deploys, initializes, mints to self, transfers to //Charlie,
//Charlie burns, publisher shuts down. Publisher cannot be the
storage provider because the provider's background coordinator
races the test's nonce sequence (surfaces as Invalid::Stale).
- examples/papi/sc-api.js: `substrateToH160` helper (forward
direction of AccountId32Mapper: keccak256(account)[12..]) for
targeting an EVM address derived from a substrate account.
- just sc-token-gated recipe and matching CI step.
- docs/design/smart-contracts.md: IS3Registry ABI table, the 4-
script test layout, and the v1 put_object_metadata user-metadata
caveat.
Issue #83.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
The workflow loads this file via `cat .github/env >> $GITHUB_ENV`, which only accepts `KEY=value` lines. The "# Smart-contract toolchain …" comment was rejected by GHA's env-file parser, failing the `set-image` job (and skipping every downstream job that depends on its container-image output). Move the explanation to examples/contracts/README.md, which already documents the version pins.
Address github-code-quality bot findings on PR #95: - sc-coverage.js: remove `challengeOffchain` from the api.js import (was kept "for parity" but never called — the script only exercises challengeCheckpoint). - sc-coverage.js: the third `nextBucketBefore` assignment was dropping its value on the floor; add the matching `assert.strictEqual(bucketC, nextBucketBefore)` so the invariant matches the bucketA / bucketB cases above. - sc-token-gated.js: remove `toHex` from the common.js import (leftover from an earlier draft that printed provider bytes). Audited all five examples/papi/sc-*.js files for unused named imports; no other findings.
Zepter caught a missing feature propagation in CI:
crate 'pallet-s3-registry-precompile'
feature 'try-runtime' must propagate to: pallet-s3-registry
I had only added the `pallet-storage-provider/try-runtime` propagation
(s3 was the second pallet dep on the precompile). pallet-s3-registry
does expose `try-runtime`, unlike pallet-drive-registry which doesn't —
so the propagation is required here.
CI build failed: Error: `solc` versions >0.8.34 are not supported, found 0.8.35 resolc 1.1.0's supported-solc range tops out at 0.8.34. The earlier pin of 0.8.35 was off by one patch. Bumping `.github/env` down and noting the upper bound in `examples/contracts/README.md`.
`sc-demo` failed in CI at step 5 (`challenge_offchain`) with `AgreementNotFound`. Root cause: the CI matrix registers //Alice (inmemory) plus //Charlie (disk) before sc-* runs, and earlier in the job `papi-drive-lifecycle` registers //Ferdie too — all with `accepting_primary=true`. `create_bucket_with_storage` then auto- matches an arbitrary one, but step 5's `challenge_offchain` looks up the agreement at `(bucket_id, //Alice)` and misses when //Charlie or //Ferdie won the match. Add `ensureSoleAcceptingProvider(api, provider)` to the setup phase of all four sc-* demos (sc-flow, sc-coverage, sc-team-drive, sc-token-gated). The helper sets `accepting_primary=false` on every other dev-key provider, making the auto-match deterministic. Idempotent on subsequent calls — the helper skips providers already non-accepting, so running multiple sc-* demos back-to-back is fine.
| @@ -0,0 +1,82 @@ | |||
| // SPDX-License-Identifier: Apache-2.0 | |||
There was a problem hiding this comment.
Should we also include necessary getter functions like query_provider_info, query_providers, query_can_accept_bytes, query_available_providers, etc...
It'll be useful for pure eth client without papi dependencies
There was a problem hiding this comment.
Can be left as a follow up, as this is purely a PoC. Tracking in #112.
`integration-tests` was running L0 demos + fs/s3 lifecycles + 4 sc-* demos sequentially per matrix entry, pushing total wall-clock past the 60-minute job timeout (last run got cancelled mid-sc-coverage at chain head #431, ~43 min in). Move the four sc-* demos into a new `sc-integration-tests` job that runs in parallel against the same runtime matrix, with its own chain + provider setup. Only //Alice (inmemory) is started — sc demos use exactly one provider, no disk provider needed. Separate Rust cache key so the two jobs don't stomp on each other. `integration-tests-complete` now needs both jobs, so a failure in either fails the rollup.
|
/cmd bench --pallet pallet_storage_provider |
|
Command "bench --pallet pallet_storage_provider" has started 🚀 See logs here |
|
Command "bench --pallet pallet_storage_provider" has failed ❌! See logs here |
|
/cmd bench --pallet pallet_storage_provider |
|
Command "bench --pallet pallet_storage_provider" has started 🚀 See logs here |
…t_storage_provider'
|
Command "bench --pallet pallet_storage_provider" has finished ✅ See logs here DetailsSubweight results:
Command output:args: Namespace(command='bench', continue_on_fail=False, quiet=False, clean=False, runtime=['web3-storage-paseo', 'storage-parachain-runtime'], pallet=['pallet_storage_provider'], steps=50, repeat=20, profile='production') |
|
/cmd bench |
|
Command "bench" has started 🚀 See logs here |
|
Command "bench" has finished ✅ See logs here DetailsSubweight results:
Command output:args: Namespace(command='bench', continue_on_fail=False, quiet=False, clean=False, runtime=['web3-storage-paseo', 'storage-parachain-runtime'], pallet=[], steps=50, repeat=20, profile='production') |
Closes #83.
Wires pallet_revive 0.13.0 into both runtimes and exposes the client-side bucket lifecycle + drive registry to Solidity contracts via two custom precompiles.
What's in
420_420_500 / 420_420_501. AccountId32Mapper as the H160 ↔ AccountId32 bridge.
0x…09010000— storage-provider (11 selectors: createBucket, createBucketWithStorage, freezeBucket, set/removeMember, requestPrimaryAgreement, top/extend/endAgreement{Pay,Burn}, challengeCheckpoint)0x…09020000— drive-registry (createDrive, deleteDrive, share/unshareDrive)0x…09030000— s3-registry (createS3Bucket, createS3BucketWithStorage, deleteS3Bucket, putObjectMetadata, deleteObjectMetadata, copyObjectMetadata)StorageMarketplace.sol— buys storage withmsg.value, tracks per-user ownership, exposesendMyAgreement(storage-provider precompile).SharedTeamDrive.sol— contract owns a drive, admin invites/kicks members, disbands on teardown (drive-registry precompile).TokenGatedDrive.sol— contract owns an S3 bucket and mints a transferable NFT-shaped access token per stored object; transfer transfers access; burn deletes the object metadata (s3-registry precompile).just sc-demo— full marketplace e2e (deploy → buyStorage → upload/challenge round-trip → endMyAgreement → provider paid)just sc-coverage— direct precompile invocation for every selector across all three precompiles; on-chain event/state asserted per calljust sc-team-drive— SharedTeamDrive e2e (createTeam → invite → kick → disband)just sc-token-gated— TokenGatedDrive e2e (initialize → mint → transfer → burn → shutdown).github/env), runs both demos across the matrix runtimes.docs/design/smart-contracts.md(address layout, ABI tables, type encoding, origin/payment model, "add a new selector" walkthrough)What's not in (follow-ups)
putObjectMetadatadrops user metadata. The Rust extrinsic takesVec<(Vec, Vec)>; the Solidity ABI for nested dynamic-bytes tuples is awkward, so the v1 selector always passes an empty vector. Use the substrate extrinsic directly if you need it.pallet_revive_eth_rpcserver. PAPI drives revive directly viaRevive.call/instantiate_with_code. A future "real dApp UX" PR can ship the JSON-RPC node so MetaMask / viem / hardhat work.checkpoint/provider_checkpointtakeBucketSnapshot + MmrProof + Vec<Signature>— needs ABI design).